/*
 * Copyright 2015 Brockmann Consult GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file was modified by Illumon.
 *
 */

#include <jni.h>
#include <Python.h>

#include "jpy_module.h"
#include "jpy_diag.h"
#include "jpy_jtype.h"
#include "jpy_jobj.h"
#include "jpy_conv.h"

#include "org_jpy_PyLib.h"
#include "org_jpy_PyLib_Diag.h"

// Note: Native org.jpy.PyLib function definition headers in this file are formatted according to the header
// generated by javah. This makes it easier to follow up changes in the header.

PyObject* PyLib_GetAttributeObject(JNIEnv* jenv, PyObject* pyValue, jstring jName);
PyObject* PyLib_CallAndReturnObject(JNIEnv *jenv, PyObject* pyValue, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses);
void PyLib_HandlePythonException(JNIEnv* jenv);
void PyLib_ThrowOOM(JNIEnv* jenv);
void PyLib_ThrowFNFE(JNIEnv* jenv, const char *file);
void PyLib_ThrowUOE(JNIEnv* jenv, const char *message);
void PyLib_ThrowRTE(JNIEnv* jenv, const char *message);
void PyLib_RedirectStdOut(void);
int copyPythonDictToJavaMap(JNIEnv *jenv, PyObject *pyDict, jobject jMap);

static int JPy_InitThreads = 0;

//#define JPy_JNI_DEBUG 1
#define JPy_JNI_DEBUG 0

// Make sure the following contants are same as in enum org.jpy.PyInputMode
#define JPy_IM_STATEMENT  256
#define JPy_IM_SCRIPT     257
#define JPy_IM_EXPRESSION 258

#define JPy_GIL_AWARE

#ifdef JPy_GIL_AWARE
    #define JPy_BEGIN_GIL_STATE  { PyGILState_STATE gilState; if (!JPy_InitThreads) {JPy_InitThreads = 1; PyEval_InitThreads(); PyEval_SaveThread(); } gilState = PyGILState_Ensure();
    #define JPy_END_GIL_STATE    PyGILState_Release(gilState); }
#else
    #define JPy_BEGIN_GIL_STATE
    #define JPy_END_GIL_STATE
#endif


/**
 * Called if the JVM loads this module.
 * Will only called if this module's code is linked into a shared library and loaded by a Java VM.
 */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved)
{
    if (JPy_JNI_DEBUG) printf("JNI_OnLoad: enter: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                              jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    if (JPy_JVM == NULL) {
        JPy_JVM = jvm;
        JPy_MustDestroyJVM = JNI_FALSE;
    } else if (JPy_JVM == jvm) {
        if (JPy_JNI_DEBUG) printf("JNI_OnLoad: warning: same JVM already running\n");
    } else {
        if (JPy_JNI_DEBUG) printf("JNI_OnLoad: warning: different JVM already running (expect weird things!)\n");
    }

    if (JPy_JNI_DEBUG) printf("JNI_OnLoad: exit: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                              jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    if (JPy_JNI_DEBUG) fflush(stdout);

    return JPY_JNI_VERSION;
}


/**
 * Called if the JVM unloads this module.
 * Will only called if this module's code is linked into a shared library and loaded by a Java VM.
 */
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* jvm, void* reserved)
{
    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JNI_OnUnload: enter: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                   jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    Py_Finalize();

    if (!JPy_MustDestroyJVM) {
        JPy_ClearGlobalVars(JPy_GetJNIEnv());
        JPy_JVM = NULL;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JNI_OnUnload: exit: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                   jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());
}



/*
 * Class:     org_jpy_PyLib
 * Method:    isPythonRunning
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isPythonRunning
  (JNIEnv* jenv, jclass jLibClass)
{
    int init;
    init = Py_IsInitialized();
    return init && JPy_Module != NULL;
}

#define  MAX_PYTHON_HOME   256
#if defined(JPY_COMPAT_33P)
wchar_t staticPythonHome[MAX_PYTHON_HOME];
#elif defined(JPY_COMPAT_27)
char staticPythonHome[MAX_PYTHON_HOME];
#endif

/*
 * Class:     org_jpy_PyLib
 * Method:    setPythonHome
 * Signature: (Ljava/lang/String;)Z
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_setPythonHome
  (JNIEnv* jenv, jclass jLibClass, jstring jPythonHome)
{
#if defined(JPY_COMPAT_33P) && !defined(JPY_COMPAT_35P)
    return 0;  // Not supported because DecodeLocale didn't exist in 3.4
#else

    #if defined(JPY_COMPAT_35P)
    const wchar_t* pythonHome = NULL;
    #elif defined(JPY_COMPAT_27)
    const char* pythonHome = NULL;
    #endif

    const char *nonWidePythonHome = NULL;
    jboolean result = 0;
    nonWidePythonHome = (*jenv)->GetStringUTFChars(jenv, jPythonHome, NULL);

    if (nonWidePythonHome != NULL) {

        #if defined(JPY_COMPAT_35P)
        pythonHome = Py_DecodeLocale(nonWidePythonHome, NULL);
        if (pythonHome != NULL) {
            if (wcslen(pythonHome) < MAX_PYTHON_HOME) {
                 wcscpy(staticPythonHome, pythonHome);
                 result = 1;
            }
            else {
                PyMem_RawFree(pythonHome);
            }
               
        }
          
        #elif defined(JPY_COMPAT_27)
        pythonHome = nonWidePythonHome;
        if (strlen(pythonHome) < MAX_PYTHON_HOME) {
            strcpy(staticPythonHome, pythonHome);
            result = 1;
        }
        #endif
        
        if (result) {
            Py_SetPythonHome(staticPythonHome);
    
            #if defined(JPY_COMPAT_35P)
            PyMem_RawFree(pythonHome);
            #endif
        }

        (*jenv)->ReleaseStringUTFChars(jenv, jPythonHome, nonWidePythonHome);
    }

    return result;
#endif
}

/*
 * Class:     org_jpy_PyLib
 * Method:    startPython0
 * Signature: ([Ljava/lang/String;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_startPython0
  (JNIEnv* jenv, jclass jLibClass, jobjectArray jPathArray)
{
    int pyInit = Py_IsInitialized();

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: entered: jenv=%p, pyInit=%d, JPy_Module=%p\n", jenv, pyInit, JPy_Module);

    if (!pyInit) {
        Py_Initialize();
        // See https://github.com/bcdev/jpy/issues/81
        PySys_SetArgvEx(0, NULL, 0);
        PyLib_RedirectStdOut();
        pyInit = Py_IsInitialized();
    }

    if (pyInit) {

        if (JPy_DiagFlags != 0) {
            printf("PyLib_startPython: global Python interpreter information:\n");
            #if defined(JPY_COMPAT_33P)
            printf("  Py_GetProgramName()     = \"%ls\"\n", Py_GetProgramName());
            printf("  Py_GetPrefix()          = \"%ls\"\n", Py_GetPrefix());
            printf("  Py_GetExecPrefix()      = \"%ls\"\n", Py_GetExecPrefix());
            printf("  Py_GetProgramFullPath() = \"%ls\"\n", Py_GetProgramFullPath());
            printf("  Py_GetPath()            = \"%ls\"\n", Py_GetPath());
            printf("  Py_GetPythonHome()      = \"%ls\"\n", Py_GetPythonHome());
            #elif defined(JPY_COMPAT_27)
            printf("  Py_GetProgramName()     = \"%s\"\n", Py_GetProgramName());
            printf("  Py_GetPrefix()          = \"%s\"\n", Py_GetPrefix());
            printf("  Py_GetExecPrefix()      = \"%s\"\n", Py_GetExecPrefix());
            printf("  Py_GetProgramFullPath() = \"%s\"\n", Py_GetProgramFullPath());
            printf("  Py_GetPath()            = \"%s\"\n", Py_GetPath());
            printf("  Py_GetPythonHome()      = \"%s\"\n", Py_GetPythonHome());
            #endif
            printf("  Py_GetVersion()         = \"%s\"\n", Py_GetVersion());
            printf("  Py_GetPlatform()        = \"%s\"\n", Py_GetPlatform());
            printf("  Py_GetCompiler()        = \"%s\"\n", Py_GetCompiler());
            printf("  Py_GetBuildInfo()       = \"%s\"\n", Py_GetBuildInfo());
        }

        // If we've got jPathArray, add all entries to Python's "sys.path"
        //
        if (jPathArray != NULL) {
            PyObject* pyPathList;
            PyObject* pyPath;
            jstring jPath;
            jsize i, pathCount;

            pathCount = (*jenv)->GetArrayLength(jenv, jPathArray);
            //printf(">> pathCount=%d\n", pathCount);
            if (pathCount > 0) {

                JPy_BEGIN_GIL_STATE

                pyPathList = PySys_GetObject("path");
                //printf(">> pyPathList=%p, len=%ld\n", pyPathList, PyList_Size(pyPathList));
                if (pyPathList != NULL) {
                    Py_INCREF(pyPathList);
                    for (i = pathCount - 1; i >= 0; i--) {
                        jPath = (*jenv)->GetObjectArrayElement(jenv, jPathArray, i);
                        //printf(">> i=%d, jPath=%p\n", i, jPath);
                        if (jPath != NULL) {
                            pyPath = JPy_FromJString(jenv, jPath);
                            //printf(">> i=%d, pyPath=%p\n", i, pyPath);
                            if (pyPath != NULL) {
                                PyList_Insert(pyPathList, 0, pyPath);
                            }
                        }
                    }
                    Py_DECREF(pyPathList);
                }
                //printf(">> pyPathList=%p, len=%ld\n", pyPathList, PyList_Size(pyPathList));
                //printf(">> pyPathList=%p, len=%ld (check)\n", PySys_GetObject("path"), PyList_Size(PySys_GetObject("path")));

                JPy_END_GIL_STATE
            }
        }

        // if the global JPy_Module is NULL, then the 'jpy' extension module has not been imported yet.
        //
        if (JPy_Module == NULL) {
            PyObject* pyModule;

            JPy_BEGIN_GIL_STATE

            // We import 'jpy' so that Python can call our PyInit_jpy() which sets up a number of
            // required global variables (including JPy_Module, see above).
            //
            pyModule = PyImport_ImportModule("jpy");
            //printf(">> pyModule=%p\n", pyModule);
            if (pyModule == NULL) {
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: failed to import module 'jpy'\n");
                if (JPy_DiagFlags != 0 && PyErr_Occurred()) {
                    PyErr_Print();
                }
                PyLib_HandlePythonException(jenv);
            }

            JPy_END_GIL_STATE
        }
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: exiting: jenv=%p, pyInit=%d, JPy_Module=%p\n", jenv, pyInit, JPy_Module);

    //printf(">> JPy_Module=%p\n", JPy_Module);

    if (!pyInit) {
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Failed to initialize Python interpreter.");
        return JNI_FALSE;
    }
    if (JPy_Module == NULL) {
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Failed to initialize Python 'jpy' module.");
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    stopPython0
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_stopPython0
  (JNIEnv* jenv, jclass jLibClass)
{
    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_stopPython: entered: JPy_Module=%p\n", JPy_Module);

    if (Py_IsInitialized()) {
        // Make sure we can get the GIL if needed before cleaning up.
        PyGILState_STATE state = PyGILState_Ensure();
        // Cleanup the JPY stateful structures and shut down the interpreter.
        JPy_free();
        Py_Finalize();
        // Make sure we reset our global flag
        JPy_InitThreads = 0;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_stopPython: exiting: JPy_Module=%p\n", JPy_Module);
}


/*
 * Class:     org_jpy_PyLib
 * Method:    getPythonVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_getPythonVersion
  (JNIEnv* jenv, jclass jLibClass)
{
    const char* version;

    version = Py_GetVersion();
    if (version == NULL) {
        return NULL;
    }

    return (*jenv)->NewStringUTF(jenv, version);
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    execScript
 * Signature: (Ljava/lang/String;)J
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_execScript
  (JNIEnv* jenv, jclass jLibClass, jstring jScript)
{
    const char* scriptChars;
    int retCode = -1;

    JPy_BEGIN_GIL_STATE

    scriptChars = (*jenv)->GetStringUTFChars(jenv, jScript, NULL);
    if (scriptChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_execScript: script='%s'\n", scriptChars);
    retCode = PyRun_SimpleString(scriptChars);
    if (retCode < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_execScript: error: PyRun_SimpleString(\"%s\") returned %d\n", scriptChars, retCode);
        // Note that we cannot retrieve last Python exception after a calling PyRun_SimpleString, see documentation of PyRun_SimpleString.
    }

error:
    if (scriptChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jScript, scriptChars);
    }

    JPy_END_GIL_STATE

    return retCode;
}


PyObject* PyLib_ConvertJavaToPythonObject(JNIEnv* jenv, jobject jObject)
{
    JPy_JType* type;
    type = JType_GetTypeForObject(jenv, jObject);
    return JType_ConvertJavaToPythonObject(jenv, type, jObject);
}

int PyLib_ConvertPythonToJavaObject(JNIEnv* jenv, PyObject* pyObject, jobject* jObject)
{
    return JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, pyObject, jObject, JNI_FALSE);
}

void dumpDict(const char* dictName, PyObject* dict)
{
    Py_ssize_t size;
    PyObject *key = NULL, *value = NULL;
    Py_ssize_t pos = 0;
    Py_ssize_t i = 0;

    if (!PyDict_Check(dict)) {
        printf(">> dumpDict: %s is not a dictionary!\n", dictName);
        return;
    }

    size = PyDict_Size(dict);
    printf(">> dumpDict: %s.size = %ld\n", dictName, size);
    while (PyDict_Next(dict, &pos, &key, &value)) {
        const char* name;
        name = JPy_AS_UTF8(key);
        printf(">> dumpDict: %s[%ld].name = '%s'\n", dictName, i, name);
        i++;
    }
}

/**
 * Get the globals from the __main__ module.
 */
PyObject *getMainGlobals() {
    PyObject* pyMainModule;
    PyObject* pyGlobals;

    JPy_BEGIN_GIL_STATE

    pyMainModule = PyImport_AddModule("__main__"); // borrowed ref

    if (pyMainModule == NULL) {
        return NULL;
    }


    pyGlobals = PyModule_GetDict(pyMainModule); // borrowed ref

    JPy_END_GIL_STATE

    return pyGlobals;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getMainGlobals
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef;

    PyObject *globals = getMainGlobals();

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, globals, &objectRef, JNI_FALSE) < 0) {
        return NULL;
    }

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_copyDict
        (JNIEnv *jenv, jclass libClass, jlong pyPointer) {
    jobject objectRef;
    PyObject *src, *copy;

    src = (PyObject*)pyPointer;

    if (!PyDict_Check(src)) {
        PyLib_ThrowUOE(jenv, "Not a dictionary!");
        return NULL;
    }

    copy = PyDict_Copy(src);

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, copy, &objectRef, JNI_FALSE) < 0) {
        return NULL;
    }

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_newDict
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef;
    PyObject *dict;

    dict = PyDict_New();

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, dict, &objectRef, JNI_FALSE) < 0) {
        return NULL;
    }

    return objectRef;
}

/**
 * Copies a Java Map<String, Object> into a new Python dictionary.
 */
PyObject *copyJavaStringObjectMapToPyDict(JNIEnv *jenv, jobject jMap) {
    PyObject *result;
    jobject entrySet, iterator, mapEntry;
    jboolean hasNext;

    result = PyDict_New();
    if (result == NULL) {
        return result;
    }

    entrySet = (*jenv)->CallObjectMethod(jenv, jMap, JPy_Map_entrySet_MID);
    if (entrySet == NULL) {
        goto error;
    }

    iterator = (*jenv)->CallObjectMethod(jenv, entrySet, JPy_Set_Iterator_MID);
    if (iterator == NULL) {
        goto error;
    }

    hasNext = (*jenv)->CallBooleanMethod(jenv, iterator, JPy_Iterator_hasNext_MID);

    while (hasNext) {
        jobject key, value;
        char const *keyChars;
        PyObject *pyKey;
        PyObject *pyValue;
        JPy_JType* type;

        mapEntry = (*jenv)->CallObjectMethod(jenv, iterator, JPy_Iterator_next_MID);
        if (mapEntry == NULL) {
            goto error;
        }

        key = (*jenv)->CallObjectMethod(jenv, mapEntry, JPy_Map_Entry_getKey_MID);
        if (key == NULL) {
            goto error;
        }

        // we require string keys
        if (!(*jenv)->IsInstanceOf(jenv, key, JPy_String_JClass)) {
            goto error;
        }
        keyChars = (*jenv)->GetStringUTFChars(jenv, (jstring)key, NULL);
        if (keyChars == NULL) {
            goto error;
        }

        pyKey = JPy_FROM_CSTR(keyChars);
        (*jenv)->ReleaseStringUTFChars(jenv, (jstring)key, keyChars);

        value = (*jenv)->CallObjectMethod(jenv, mapEntry, JPy_Map_Entry_getValue_MID);

        type = JType_GetTypeForObject(jenv, value);
        pyValue = JType_ConvertJavaToPythonObject(jenv, type, value);

        PyDict_SetItem(result, pyKey, pyValue);

        hasNext = (*jenv)->CallBooleanMethod(jenv, iterator, JPy_Iterator_hasNext_MID);
    }

    return result;

error:
    if (result != NULL) {
        Py_XDECREF(result);
    }
    return NULL;
}

int copyPythonDictToJavaMap(JNIEnv *jenv, PyObject *pyDict, jobject jMap) {
    PyObject *pyKey, *pyValue;
    Py_ssize_t pos = 0;
    Py_ssize_t dictSize;
    jobject *jValues = NULL;
    jobject *jKeys = NULL;
    int ii;
    jboolean exceptionAlready = JNI_FALSE;
    jthrowable savedException = NULL;
    int retcode = -1;

    if (!PyDict_Check(pyDict)) {
        PyLib_ThrowUOE(jenv, "PyObject is not a dictionary!");
        return -1;
    }

    dictSize = PyDict_Size(pyDict);

    jKeys = malloc(dictSize * sizeof(jobject));
    jValues = malloc(dictSize * sizeof(jobject));
    if (jKeys == NULL || jValues == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    exceptionAlready = (*jenv)->ExceptionCheck(jenv);
    if (exceptionAlready) {
        // save the exception away, because otherwise the conversion methods might spuriously fail
        savedException = (*jenv)->ExceptionOccurred(jenv);
        (*jenv)->ExceptionClear(jenv);
    }

    // first convert everything
    ii = 0;
    while (PyDict_Next(pyDict, &pos, &pyKey, &pyValue)) {
        if (JPy_AsJObjectWithClass(jenv, pyKey, &(jKeys[ii]), JPy_String_JClass) < 0) {
            // an error occurred
            goto error;
        }
        if (JPy_AsJObject(jenv, pyValue, &(jValues[ii]), JNI_TRUE) < 0) {
            // an error occurred
            goto error;
        }
        ii++;
    }

    // now that we've converted, clear out the map and repopulate it
    (*jenv)->CallVoidMethod(jenv, jMap, JPy_Map_clear_MID);
    for (ii = 0; ii < dictSize; ++ii) {
        // since the map is cleared, we want to plow through all of the put operations
        (*jenv)->CallObjectMethod(jenv, jMap, JPy_Map_put_MID, jKeys[ii], jValues[ii]);
    }
    // and we are successful!
    retcode = 0;

error:
    if (exceptionAlready) {
        // restore our original exception
        (*jenv)->Throw(jenv, savedException);
    }

    free(jKeys);
    free(jValues);
    return retcode;
}

typedef PyObject * (*DoRun)(const void *,int,PyObject*,PyObject*);

/**
 * After setting up the globals and locals, calls the provided function.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 *
 */
jlong executeInternal(JNIEnv* jenv, jclass jLibClass, jint jStart, jobject jGlobals, jobject jLocals, DoRun runFunction, void *runArg) {
    PyObject *pyReturnValue;
    PyObject *pyGlobals;
    PyObject *pyLocals;
    int start;
    jboolean decGlobals, decLocals, copyGlobals, copyLocals;

    JPy_BEGIN_GIL_STATE

    decGlobals = decLocals = JNI_FALSE;
    copyGlobals = copyLocals = JNI_FALSE;
    pyGlobals = NULL;
    pyLocals = NULL;
    pyReturnValue = NULL;

    if (jGlobals == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using main globals\n");
        pyGlobals = getMainGlobals();
        if (pyGlobals == NULL) {
            PyLib_HandlePythonException(jenv);
            goto error;
        }
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_PyObject_JClass)) {
        // if we are an instance of PyObject, just use the object
        pyGlobals = (PyObject *)((*jenv)->CallLongMethod(jenv, jGlobals, JPy_PyObject_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyObject globals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_PyDictWrapper_JClass)) {
        // if we are an instance of a wrapped dictionary, just use the underlying dictionary
        pyGlobals = (PyObject *)((*jenv)->CallLongMethod(jenv, jGlobals, JPy_PyDictWrapper_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyDictWrapper globals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_Map_JClass)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using Java Map globals\n");
        // this is a java Map and we need to convert it
        pyGlobals = copyJavaStringObjectMapToPyDict(jenv, jGlobals);
        if (pyGlobals == NULL) {
            PyLib_ThrowRTE(jenv, "Could not convert globals from Java Map to Python dictionary");
            goto error;
        }
        copyGlobals = decGlobals = JNI_TRUE;
    } else {
        PyLib_ThrowUOE(jenv, "Unsupported globals type");
        goto error;
    }

    // if we have no locals specified, using globals for it matches the behavior of eval, "The expression argument is
    // parsed and evaluated as a Python expression (technically speaking, a condition list) using the globals and
    // locals dictionaries as global and local namespace. ... If the locals dictionary is omitted it defaults to the
    // globals dictionary. If both dictionaries are omitted, the expression is executed in the environment where eval()
    // is called. The return value is the result of the evaluated expression.
    if (jLocals == NULL) {
        pyLocals = pyGlobals;
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using globals for locals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_PyObject_JClass)) {
        // if we are an instance of PyObject, just use the object
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyObject locals\n");
        pyLocals = (PyObject *)((*jenv)->CallLongMethod(jenv, jLocals, JPy_PyObject_GetPointer_MID));
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_PyDictWrapper_JClass)) {
        // if we are an instance of a wrapped dictionary, just use the underlying dictionary
        pyLocals = (PyObject *)((*jenv)->CallLongMethod(jenv, jLocals, JPy_PyDictWrapper_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyDictWrapper locals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_Map_JClass)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using Java Map locals\n");
        // this is a java Map and we need to convert it
        pyLocals = copyJavaStringObjectMapToPyDict(jenv, jLocals);
        if (pyLocals == NULL) {
            PyLib_ThrowRTE(jenv, "Could not convert locals from Java Map to Python dictionary");
            goto error;
        }
        copyLocals = decLocals = JNI_TRUE;
    } else {
        PyLib_ThrowUOE(jenv, "Unsupported locals type");
        goto error;
    }

    start = jStart == JPy_IM_STATEMENT ? Py_single_input :
            jStart == JPy_IM_SCRIPT ? Py_file_input :
            Py_eval_input;

    pyReturnValue = runFunction(runArg, start, pyGlobals, pyLocals);
    if (pyReturnValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: Handle Python Exception\n");
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (copyGlobals) {
        copyPythonDictToJavaMap(jenv, pyGlobals, jGlobals);
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: copied back Java global\n");
    }
    if (copyLocals) {
        copyPythonDictToJavaMap(jenv, pyLocals, jLocals);
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: copied back Java locals\n");
    }
    if (decGlobals) {
        Py_XDECREF(pyGlobals);
    }
    if (decLocals) {
        Py_XDECREF(pyLocals);
    }

    JPy_END_GIL_STATE

    return (jlong) pyReturnValue;
}

PyObject *pyRunStringWrapper(const char *code, int start, PyObject *globals, PyObject *locals) {
    PyObject *result = PyRun_String(code, start, globals, locals);
    return result;
}

/**
 * Calls PyRun_String under the covers to execute the string contents.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 */
JNIEXPORT
jlong JNICALL Java_org_jpy_PyLib_executeCode
        (JNIEnv* jenv, jclass jLibClass, jstring jCode, jint jStart, jobject jGlobals, jobject jLocals) {
    const char *codeChars;
    jlong result;

    codeChars = (*jenv)->GetStringUTFChars(jenv, jCode, NULL);
    if (codeChars == NULL) {
        PyLib_ThrowOOM(jenv);
        return NULL;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeCode: code='%s'\n", codeChars);

    result = executeInternal(jenv, jLibClass, jStart, jGlobals, jLocals, (DoRun)pyRunStringWrapper, codeChars);

    if (codeChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jCode, codeChars);
    }

    return result;
}

typedef struct {
    FILE *fp;
    const char *filechars;
} RunFileArgs;

PyObject *pyRunFileWrapper(RunFileArgs *args, int start, PyObject *globals, PyObject *locals) {
    return PyRun_File(args->fp, args->filechars, start, globals, locals);
}

/**
 * Calls PyRun_Script under the covers to execute the script contents.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_executeScript
        (JNIEnv* jenv, jclass jLibClass, jstring jFile, jint jStart, jobject jGlobals, jobject jLocals) {
    RunFileArgs runFileArgs;
    jlong result = 0;

    runFileArgs.fp = NULL;
    runFileArgs.filechars = NULL;

    runFileArgs.filechars = (*jenv)->GetStringUTFChars(jenv, jFile, NULL);
    if (runFileArgs.filechars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    runFileArgs.fp = fopen(runFileArgs.filechars, "r");
    if (!runFileArgs.fp) {
        PyLib_ThrowFNFE(jenv, runFileArgs.filechars);
        goto error;
    }

    result = executeInternal(jenv, jLibClass, jStart, jGlobals, jLocals, (DoRun)pyRunFileWrapper, &runFileArgs);

error:
    if (runFileArgs.filechars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jFile, runFileArgs.filechars);
    }
    if (runFileArgs.fp != NULL) {
        fclose(runFileArgs.fp);
    }
    return result;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    incRef
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_incRef
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    Py_ssize_t refCount;

    pyObject = (PyObject*) objId;

    if (Py_IsInitialized()) {
        JPy_BEGIN_GIL_STATE

        refCount = pyObject->ob_refcnt;
        JPy_DIAG_PRINT(JPy_DIAG_F_MEM, "Java_org_jpy_PyLib_incRef: pyObject=%p, refCount=%d, type='%s'\n", pyObject, refCount, Py_TYPE(pyObject)->tp_name);
        Py_INCREF(pyObject);

        JPy_END_GIL_STATE
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_incRef: error: no interpreter: pyObject=%p\n", pyObject);
    }
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    decRef
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_decRef
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    Py_ssize_t refCount;

    pyObject = (PyObject*) objId;

    if (Py_IsInitialized()) {
        JPy_BEGIN_GIL_STATE

        refCount = pyObject->ob_refcnt;
        if (refCount <= 0) {
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRef: error: refCount <= 0: pyObject=%p, refCount=%d\n", pyObject, refCount);
        } else {
            JPy_DIAG_PRINT(JPy_DIAG_F_MEM, "Java_org_jpy_PyLib_decRef: pyObject=%p, refCount=%d, type='%s'\n", pyObject, refCount, Py_TYPE(pyObject)->tp_name);
            Py_DECREF(pyObject);
        }

        JPy_END_GIL_STATE
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRef: error: no interpreter: pyObject=%p\n", pyObject);
    }
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getIntValue
 * Signature: (J)I
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_getIntValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jint value;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;
    value = (jint) JPy_AS_CLONG(pyObject);

    JPy_END_GIL_STATE

    return value;
}

/**
 * Used to convert a python object into it's corresponding boolean.  If the PyObject is not a boolean;
 * then return false.
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_getBooleanValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jboolean value;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;
    if (PyBool_Check(pyObject)) {
        value = (pyObject == Py_True) ? JNI_TRUE : JNI_FALSE;
    } else {
        value = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return value;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getDoubleValue
 * Signature: (J)D
 */
JNIEXPORT jdouble JNICALL Java_org_jpy_PyLib_getDoubleValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jdouble value;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;
    value = (jdouble) PyFloat_AsDouble(pyObject);

    JPy_END_GIL_STATE

    return value;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getStringValue
 * Signature: (J)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_getStringValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jstring jString;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    if (JPy_AsJString(jenv, pyObject, &jString) < 0) {
        jString = NULL;
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getStringValue: error: failed to convert Python object to Java String\n");
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return jString;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getObjectValue
 * Signature: (J)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getObjectValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jobject jObject;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    if (JObj_Check(pyObject)) {
        jObject = ((JPy_JObj*) pyObject)->objectRef;
    } else {
        if (JPy_AsJObject(jenv, pyObject, &jObject, JNI_FALSE) < 0) {
            jObject = NULL;
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getObjectValue: error: failed to convert Python object to Java Object\n");
            PyLib_HandlePythonException(jenv);
        }
    }

    JPy_END_GIL_STATE

    return jObject;
}

/**
 * Returns true if this object can be converted from a Python object into a Java object (or primitive);
 * if this returns false, when you fetch an Object from Python it will be a PyObject wrapper.
 *
 * objId is a pointer to a PyObject.
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isConvertible
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jboolean result;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    result = pyObject == Py_None || JObj_Check(pyObject) || PyBool_Check(pyObject) ||
                 JPy_IS_CLONG(pyObject) || PyFloat_Check(pyObject) || JPy_IS_STR(pyObject) ? JNI_TRUE : JNI_FALSE;


    JPy_END_GIL_STATE

    return result;
}

/**
 * Gets the Python type object of specified objId.
 *
 * objId is a pointer to a PyObject.
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_getType
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;

    JPy_BEGIN_GIL_STATE

    pyObject = ((PyObject*) objId)->ob_type;

    JPy_END_GIL_STATE

    return (jlong)pyObject;
}

/**
 * Evaluate PyDict_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @return true if objId is a python dictionary
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyDictCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyDict_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyList_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @return true if objId is a python list
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyListCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyList_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyBool_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python boolean
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyBoolCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyBool_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Check equality against Py_None and a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a None
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyNoneCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (Py_None == (((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyInt_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python int
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyIntCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    int check;

    JPy_BEGIN_GIL_STATE

#ifdef JPY_COMPAT_27
    check = PyInt_Check(((PyObject*) objId));
#else
    check = JPy_IS_CLONG(((PyObject*) objId));
#endif

    JPy_END_GIL_STATE

    return check ? (jboolean)JNI_TRUE : (jboolean)JNI_FALSE;
}

/**
 * Evaluate PyLong_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python long
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyLongCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyLong_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyFloat_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python float
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyFloatCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyFloat_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyString_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python String
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyStringCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (JPy_IS_STR(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyCallable_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python callable
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyCallableCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE

    if (PyCallable_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Runs the str function on a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the class object for PyLib
 * @param objId a pointer to a python object
 * @return the Python toString of this object
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_str
  (JNIEnv* jenv, jclass jLibClass, jlong objId) {
    PyObject *pyObject;
    jobject jObject;
    PyObject *pyStr;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject *) objId;

    pyStr = PyObject_Str(pyObject);
    if (pyStr) {
        jObject = (*jenv)->NewStringUTF(jenv, JPy_AS_UTF8(pyStr));
        Py_DECREF(pyStr);
    } else {
        jObject = NULL;
    }


    JPy_END_GIL_STATE

    return jObject;
}


/**
 * Runs the repr function on a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the class object for PyLib
 * @param objId a pointer to a python object
 * @return the Python representation string of this object
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_repr
  (JNIEnv* jenv, jclass jLibClass, jlong objId) {
    PyObject *pyObject;
    jobject jObject;
    PyObject *pyStr;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject *) objId;

    pyStr = PyObject_Repr(pyObject);
    if (pyStr) {
        jObject = (*jenv)->NewStringUTF(jenv, JPy_AS_UTF8(pyStr));
        Py_DECREF(pyStr);
    } else {
        jObject = NULL;
    }


    JPy_END_GIL_STATE

    return jObject;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    getObjectArrayValue
 * Signature: (JLjava/lang/Class;)[Ljava/lang/Object;
 */
JNIEXPORT jobjectArray JNICALL Java_org_jpy_PyLib_getObjectArrayValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jclass itemClassRef)
{
    PyObject* pyObject;
    jobject jObject;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    if (pyObject == Py_None) {
        jObject = NULL;
    } else if (JObj_Check(pyObject)) {
        jObject = ((JPy_JObj*) pyObject)->objectRef;
    } else if (PySequence_Check(pyObject)) {
        PyObject* pyItem;
        jobject jItem;
        jint i, length;

        length = PySequence_Length(pyObject);

        jObject = (*jenv)->NewObjectArray(jenv, length, itemClassRef, NULL);

        for (i = 0; i < length; i++) {
            pyItem = PySequence_GetItem(pyObject, i);
            if (pyItem == NULL) {
                (*jenv)->DeleteLocalRef(jenv, jObject);
                jObject = NULL;
                goto error;
            }
            if (JPy_AsJObject(jenv, pyItem, &jItem, JNI_FALSE) < 0) {
                (*jenv)->DeleteLocalRef(jenv, jObject);
                jObject = NULL;
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getObjectArrayValue: error: failed to convert Python item to Java Object\n");
                PyLib_HandlePythonException(jenv);
                goto error;
            }
            Py_XDECREF(pyItem);
            (*jenv)->SetObjectArrayElement(jenv, jObject, i, jItem);
            if ((*jenv)->ExceptionCheck(jenv)) {
                (*jenv)->DeleteLocalRef(jenv, jObject);
                jObject = NULL;
                goto error;
            }
        }
    } else {
        jObject = NULL;
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "python object cannot be converted to Object[]");
    }

error:
    JPy_END_GIL_STATE

    return jObject;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getModule
 * Signature: (Ljava/lang/String;)J
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_importModule
  (JNIEnv* jenv, jclass jLibClass, jstring jName)
{
    PyObject* pyName;
    PyObject* pyModule = NULL;
    const char* nameChars;

    JPy_BEGIN_GIL_STATE

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_importModule: name='%s'\n", nameChars);
    /* Note: pyName is a new reference */
    pyName = JPy_FROM_CSTR(nameChars);
    /* Note: pyModule is a new reference */
    pyModule = PyImport_Import(pyName);
    if (pyModule == NULL) {
        PyLib_HandlePythonException(jenv);
    }
    Py_XDECREF(pyName);

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_END_GIL_STATE

    return (jlong) pyModule;
}




/*
 * Class:     org_jpy_python_PyLib
 * Method:    getAttributeValue
 * Signature: (JLjava/lang/String;)J
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_getAttributeObject
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    PyObject* pyValue;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;
    pyValue = PyLib_GetAttributeObject(jenv, pyObject, jName);

    JPy_END_GIL_STATE

    return (jlong) pyValue;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getAttributeValue
 * Signature: (JLjava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getAttributeValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName, jclass jValueClass)
{
    PyObject* pyObject;
    PyObject* pyValue;
    jobject jReturnValue;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    pyValue = PyLib_GetAttributeObject(jenv, pyObject, jName);
    if (pyValue == NULL) {
        jReturnValue = NULL;
        goto error;
    }

    if (JPy_AsJObjectWithClass(jenv, pyValue, &jReturnValue, jValueClass) < 0) {
        jReturnValue = NULL;
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getAttributeValue: error: failed to convert attribute value\n");
        PyLib_HandlePythonException(jenv);
    }

error:
    JPy_END_GIL_STATE

    return jReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    setAttributeValue
 * Signature: (JLjava/lang/String;J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_setAttributeValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName, jobject jValue, jclass jValueClass)
{
    PyObject* pyObject;
    const char* nameChars;
    PyObject* pyValue;
    JPy_JType* valueType;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_setAttributeValue: objId=%p, name='%s', jValue=%p, jValueClass=%p\n", pyObject, nameChars, jValue, jValueClass);

    if (jValueClass != NULL) {
        valueType = JType_GetType(jenv, jValueClass, JNI_FALSE);
    } else {
        valueType = NULL;
    }

    if (valueType != NULL) {
        pyValue = JPy_FromJObjectWithType(jenv, jValue, valueType);
    } else {
        pyValue = JPy_FromJObject(jenv, jValue);
    }

    if (pyValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_setAttributeValue: error: attribute '%s': Java object not convertible\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    if (PyObject_SetAttrString(pyObject, nameChars, pyValue) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_setAttributeValue: error: PyObject_SetAttrString failed on attribute '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_END_GIL_STATE
}

/**
 * Deletes an attribute from an object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @param jName the java string naming the attribute to delete
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_delAttribute
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    const char* nameChars;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_delAttribute: objId=%p, name='%s'\n", pyObject, nameChars);

    if (PyObject_DelAttrString(pyObject, nameChars) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_delAttribute: error: PyObject_DelAttrString failed on attribute '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }


    JPy_END_GIL_STATE
}

/*
 * Checks for an attribute's existence.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @param jName the java string naming the attribute to delete
 *
 * @return true if the attribute exists on this object
 */

JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_hasAttribute
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    const char* nameChars;
    jboolean result = JNI_FALSE;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_delAttribute: objId=%p, name='%s'\n", pyObject, nameChars);

    result = PyObject_HasAttrString(pyObject, nameChars) ? JNI_TRUE : JNI_FALSE;

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_END_GIL_STATE

    return result;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    call
 * Signature: (JZLjava/lang/String;I[Ljava/lang/Object;[Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_callAndReturnObject
  (JNIEnv *jenv, jclass jLibClass, jlong objId, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses)
{
    PyObject* pyObject;
    PyObject* pyReturnValue;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;
    pyReturnValue = PyLib_CallAndReturnObject(jenv, pyObject, isMethodCall, jName, argCount, jArgs, jParamClasses);

    JPy_END_GIL_STATE

    return (jlong) pyReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    callAndReturnValue
 * Signature: (JZLjava/lang/String;I[Ljava/lang/Object;[Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_callAndReturnValue
  (JNIEnv *jenv, jclass jLibClass, jlong objId, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses, jclass jReturnClass)
{
    PyObject* pyObject;
    PyObject* pyReturnValue;
    jobject jReturnValue;

    JPy_BEGIN_GIL_STATE

    pyObject = (PyObject*) objId;

    pyReturnValue = PyLib_CallAndReturnObject(jenv, pyObject, isMethodCall, jName, argCount, jArgs, jParamClasses);
    if (pyReturnValue == NULL) {
        jReturnValue = NULL;
        goto error;
    }

    if (JPy_AsJObjectWithClass(jenv, pyReturnValue, &jReturnValue, jReturnClass) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_callAndReturnValue: error: failed to convert attribute value\n");
        PyLib_HandlePythonException(jenv);
        Py_DECREF(pyReturnValue);
        jReturnValue = NULL;
        goto error;
    }

error:
    JPy_END_GIL_STATE

    return jReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getDiagFlags
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_00024Diag_getFlags
  (JNIEnv *jenv, jclass classRef)
{
    return JPy_DiagFlags;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    setDiagFlags
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_00024Diag_setFlags
  (JNIEnv *jenv, jclass classRef, jint flags)
{
    JPy_DiagFlags = flags;
}

////////////////////////////////////////////////////////////////////////////////////
// Helpers that also throw Java exceptions


PyObject* PyLib_GetAttributeObject(JNIEnv* jenv, PyObject* pyObject, jstring jName)
{
    PyObject* pyValue = NULL;
    const char* nameChars;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_GetAttributeObject: objId=%p, name='%s'\n", pyObject, nameChars);
    /* Note: pyValue is a new reference */
    pyValue = PyObject_GetAttrString(pyObject, nameChars);
    if (pyValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_GetAttributeObject: error: attribute not found '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
    }
error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }
    return pyValue;
}

PyObject* PyLib_CallAndReturnObject(JNIEnv *jenv, PyObject* pyObject, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses)
{
    PyObject* pyCallable = NULL;
    PyObject* pyArgs = NULL;
    PyObject* pyArg;
    PyObject* pyReturnValue = Py_None;
    const char* nameChars;
    jint i;
    jobject jArg;
    jclass jParamClass;
    JPy_JType* paramType;

    pyReturnValue = NULL;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_CallAndReturnObject: objId=%p, isMethodCall=%d, name='%s', argCount=%d\n", pyObject, isMethodCall, nameChars, argCount);

    pyArgs = NULL;

    // Note: pyCallable is a new reference
    pyCallable = PyObject_GetAttrString(pyObject, nameChars);
    if (pyCallable == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: function or method not found: '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    if (!PyCallable_Check(pyCallable)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: object is not callable: '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    pyArgs = PyTuple_New(argCount);
    for (i = 0; i < argCount; i++) {
        jArg = (*jenv)->GetObjectArrayElement(jenv, jArgs, i);

        if (jParamClasses != NULL) {
            jParamClass = (*jenv)->GetObjectArrayElement(jenv, jParamClasses, i);
        } else {
            jParamClass = NULL;
        }

        if (jParamClass != NULL) {
            paramType = JType_GetType(jenv, jParamClass, JNI_FALSE);
            if (paramType == NULL) {
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: callable '%s': argument %d: failed to retrieve type\n", nameChars, i);
                PyLib_HandlePythonException(jenv);
                goto error;
            }
            pyArg = JPy_FromJObjectWithType(jenv, jArg, paramType);

            // We must keep unchanged the reference counter when calling a Python method
            // with a complex Python object as parameter. 
            // Per example:
            // PyObject dt_X = kycProcessor.getDataFrame(inputColumns.toArray(new String[0]));
            // for (DataFrameColumn column : values) {
            //    kycProcessor.addColumn(dt_X, column.getName(), column.getValues());
            // }
            if (paramType == JPy_JPyObject && paramType->componentType == NULL) {
                Py_INCREF(pyArg);
            } 

            (*jenv)->DeleteLocalRef(jenv, jParamClass);
        } else {
            pyArg = JPy_FromJObject(jenv, jArg);
        }

        (*jenv)->DeleteLocalRef(jenv, jArg);

        if (pyArg == NULL) {
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: callable '%s': argument %d: failed to convert Java into Python object\n", nameChars, i);
            PyLib_HandlePythonException(jenv);
            goto error;
        }

        // pyArg reference stolen here
        PyTuple_SetItem(pyArgs, i, pyArg);
    }

    // Check why: for some reason, we don't need the following code to invoke object methods.
    /*
    if (isMethodCall) {
        PyObject* pyMethod;

        pyMethod = PyMethod_New(pyCallable, pyObject);
        if (pyMethod == NULL) {
            JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_CallAndReturnObject: error: callable '%s': no memory\n", nameChars);
            PyLib_HandlePythonException(jenv);
            goto error;
        }
        Py_DECREF(pyCallable);
        pyCallable = pyMethod;
    }
    */

    pyReturnValue = PyObject_CallObject(pyCallable, argCount > 0 ? pyArgs : NULL);
    if (pyReturnValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: callable '%s': call returned NULL\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    Py_INCREF(pyReturnValue);

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }
    Py_XDECREF(pyCallable);
    Py_XDECREF(pyArgs);

    return pyReturnValue;
}

#if defined(JPY_COMPAT_33P)

char* PyLib_ObjToChars(PyObject* pyObj, PyObject** pyNewRef)
{
    char* chars = NULL;
    if (pyObj != NULL) {
        PyObject* pyObjStr = PyObject_Str(pyObj);
        if (pyObjStr != NULL) {
            PyObject* pyObjUtf8 = PyUnicode_AsEncodedString(pyObjStr, "utf-8", "replace");
            if (pyObjUtf8 != NULL) {
                chars = PyBytes_AsString(pyObjUtf8);
                *pyNewRef = pyObjUtf8;
            }
            Py_XDECREF(pyObjStr);
        }
    }
    return chars;
}

#elif defined(JPY_COMPAT_27)

char* PyLib_ObjToChars(PyObject* pyObj, PyObject** pyNewRef)
{
    char* chars = NULL;
    if (pyObj != NULL) {
        PyObject* pyObjStr = PyObject_Str(pyObj);
        if (pyObjStr != NULL) {
            chars = PyBytes_AsString(pyObjStr);
            *pyNewRef = pyObjStr;
        }
    }
    return chars;
}

#else

#error JPY_VERSION_ERROR

#endif

#define JPY_NOT_AVAILABLE_MSG "<not available>"
#define JPY_NOT_AVAILABLE_MSG_LEN strlen(JPY_NOT_AVAILABLE_MSG)
#define JPY_ERR_BASE_MSG "Error in Python interpreter"
#define JPY_NO_INFO_MSG JPY_ERR_BASE_MSG ", no information available"
#define JPY_INFO_ALLOC_FAILED_MSG JPY_ERR_BASE_MSG ", failed to allocate information text"

void PyLib_HandlePythonException(JNIEnv* jenv)
{
    PyObject* pyType = NULL;
    PyObject* pyValue = NULL;
    PyObject* pyTraceback = NULL;

    PyObject* pyTypeUtf8 = NULL;
    PyObject* pyValueUtf8 = NULL;
    PyObject* pyLinenoUtf8 = NULL;
    PyObject* pyFilenameUtf8 = NULL;
    PyObject* pyNamespaceUtf8 = NULL;
    char* typeChars = NULL;
    char* valueChars = NULL;
    char* linenoChars = NULL;
    char* filenameChars = NULL;
    char* namespaceChars = NULL;

    jclass jExceptionClass;

    if (PyErr_Occurred() == NULL) {
        return;
    }

    PyErr_Fetch(&pyType, &pyValue, &pyTraceback);
    //printf("M1: pyType=%p, pyValue=%p, pyTraceback=%p\n", pyType, pyValue, pyTraceback);
    //printf("U1: pyType=%s, pyValue=%s, pyTraceback=%s\n", Py_TYPE(pyType)->tp_name, Py_TYPE(pyValue)->tp_name, pyTraceback != NULL ? Py_TYPE(pyTraceback)->tp_name : "?");
    PyErr_NormalizeException(&pyType, &pyValue, &pyTraceback);
    //printf("M2: pyType=%p, pyValue=%p, pyTraceback=%p\n", pyType, pyValue, pyTraceback);
    //printf("U2: pyType=%s, pyValue=%s, pyTraceback=%s\n", Py_TYPE(pyType)->tp_name, Py_TYPE(pyValue)->tp_name, pyTraceback != NULL ? Py_TYPE(pyTraceback)->tp_name : "?");

    typeChars = PyLib_ObjToChars(pyType, &pyTypeUtf8);
    valueChars = PyLib_ObjToChars(pyValue, &pyValueUtf8);
    if (PyObject_TypeCheck(pyValue, (PyTypeObject*) PyExc_KeyError)) {
        jExceptionClass = JPy_KeyError_JClass;
    } else if (PyObject_TypeCheck(pyValue, (PyTypeObject*) PyExc_StopIteration)) {
        jExceptionClass = JPy_StopIteration_JClass;
    } else {
        jExceptionClass = JPy_RuntimeException_JClass;
    }

    if (pyTraceback != NULL) {
        PyObject* pyFrame = NULL;
        PyObject* pyCode = NULL;
        linenoChars = PyLib_ObjToChars(PyObject_GetAttrString(pyTraceback, "tb_lineno"), &pyLinenoUtf8);
        pyFrame = PyObject_GetAttrString(pyTraceback, "tb_frame");
        if (pyFrame != NULL) {
            pyCode = PyObject_GetAttrString(pyFrame, "f_code");
            if (pyCode != NULL) {
                filenameChars = PyLib_ObjToChars(PyObject_GetAttrString(pyCode, "co_filename"), &pyFilenameUtf8);
                namespaceChars = PyLib_ObjToChars(PyObject_GetAttrString(pyCode, "co_name"), &pyNamespaceUtf8);
            }
        }
        Py_XDECREF(pyCode);
        Py_XDECREF(pyFrame);
    }

    //printf("U2: typeChars=%s, valueChars=%s, linenoChars=%s, filenameChars=%s, namespaceChars=%s\n",
    //       typeChars, valueChars, linenoChars, filenameChars, namespaceChars);

    if (typeChars != NULL || valueChars != NULL
        || linenoChars != NULL || filenameChars != NULL || namespaceChars != NULL) {
        char* javaMessage;
        javaMessage = PyMem_New(char,
                                (typeChars != NULL ? strlen(typeChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (valueChars != NULL ? strlen(valueChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (linenoChars != NULL ? strlen(linenoChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (filenameChars != NULL ? strlen(filenameChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (namespaceChars != NULL ? strlen(namespaceChars) : JPY_NOT_AVAILABLE_MSG_LEN) + 80);
        if (javaMessage != NULL) {
            sprintf(javaMessage,
                    JPY_ERR_BASE_MSG ":\n"
                    "Type: %s\n"
                    "Value: %s\n"
                    "Line: %s\n"
                    "Namespace: %s\n"
                    "File: %s",
                    typeChars != NULL ? typeChars : JPY_NOT_AVAILABLE_MSG,
                    valueChars != NULL ? valueChars : JPY_NOT_AVAILABLE_MSG,
                    linenoChars != NULL ? linenoChars : JPY_NOT_AVAILABLE_MSG,
                    namespaceChars != NULL ? namespaceChars : JPY_NOT_AVAILABLE_MSG,
                    filenameChars != NULL ? filenameChars : JPY_NOT_AVAILABLE_MSG);
            (*jenv)->ThrowNew(jenv, jExceptionClass, javaMessage);
            PyMem_Del(javaMessage);
        } else {
            (*jenv)->ThrowNew(jenv, jExceptionClass, JPY_INFO_ALLOC_FAILED_MSG);
        }
    } else {
        (*jenv)->ThrowNew(jenv, jExceptionClass, JPY_NO_INFO_MSG);
    }

    Py_XDECREF(pyType);
    Py_XDECREF(pyValue);
    Py_XDECREF(pyTraceback);
    Py_XDECREF(pyTypeUtf8);
    Py_XDECREF(pyValueUtf8);
    Py_XDECREF(pyLinenoUtf8);
    Py_XDECREF(pyFilenameUtf8);
    Py_XDECREF(pyNamespaceUtf8);

    PyErr_Clear();
}

/**
 * Throw an OutOfMemoryError.
 * @param jenv the jni environment
 */
void PyLib_ThrowOOM(JNIEnv* jenv) {
    (*jenv)->ThrowNew(jenv, JPy_OutOfMemoryError_JClass, "Out of memory");
}

/**
 * Throw a FileNotFoundException.
 * @param jenv the jni environment
 */
void PyLib_ThrowFNFE(JNIEnv* jenv, const char *file) {
    (*jenv)->ThrowNew(jenv, JPy_FileNotFoundException_JClass, file);
}

/**
 * Throw an UnsupportedOperationException.
 * @param jenv the jni environment
 * @param message the exception message
 */
void PyLib_ThrowUOE(JNIEnv* jenv, const char *message) {
    (*jenv)->ThrowNew(jenv, JPy_UnsupportedOperationException_JClass, message);
}

/**
 * Throw an UnsupportedOperationException.
 * @param jenv the jni environment
 * @param message the exception message
 */
void PyLib_ThrowRTE(JNIEnv* jenv, const char *message) {
    (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, message);
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Redirect stdout

static PyObject* JPrint_write(PyObject* self, PyObject* args)
{
    if (stdout != NULL) {
        const char* text;
        if (!PyArg_ParseTuple(args, "s", &text)) {
            return NULL;
        }
        fprintf(stdout, "%s", text);
    }
    return Py_BuildValue("");
}

static PyObject* JPrint_flush(PyObject* self, PyObject* args)
{
    if (stdout != NULL) {
        fflush(stdout);
    }
    return Py_BuildValue("");
}

static PyMethodDef JPrint_Functions[] = {

    {"write",       (PyCFunction) JPrint_write, METH_VARARGS,
                    "Internal function. Used to print to stdout in embedded mode."},

    {"flush",       (PyCFunction) JPrint_flush, METH_VARARGS,
                    "Internal function. Used to flush to stdout in embedded mode."},

    {NULL, NULL, 0, NULL} /*Sentinel*/
};

#define JPY_STDOUT_MODULE_NAME "jpy_stdout"
#define JPY_STDOUT_MODULE_DOC  "Redirect 'stdout' to the console in embedded mode"

#if defined(JPY_COMPAT_33P)
static struct PyModuleDef JPrint_ModuleDef =
{
    PyModuleDef_HEAD_INIT,
    JPY_STDOUT_MODULE_NAME, /* Name of the Python JPy_Module */
    JPY_STDOUT_MODULE_DOC,  /* Module documentation */
    -1,                 /* Size of per-interpreter state of the JPy_Module, or -1 if the JPy_Module keeps state in global variables. */
    JPrint_Functions,    /* Structure containing global jpy-functions */
    NULL,     // m_reload
    NULL,     // m_traverse
    NULL,     // m_clear
    NULL      // m_free
};
#endif

void PyLib_RedirectStdOut(void)
{
    PyObject* module;
#if defined(JPY_COMPAT_33P)
    module = PyModule_Create(&JPrint_ModuleDef);
#elif defined(JPY_COMPAT_27)
    module = Py_InitModule3(JPY_STDOUT_MODULE_NAME, JPrint_Functions, JPY_STDOUT_MODULE_DOC);
#else
    #error JPY_VERSION_ERROR
#endif
    PySys_SetObject("stdout", module);
    PySys_SetObject("stderr", module);
}
